package com.songyang;

import com.songyang.blog.util.IpUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.tomcat.util.http.LegacyCookieProcessor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.scheduling.annotation.EnableScheduling;

import java.util.List;
import java.util.concurrent.TimeUnit;

@SpringBootApplication
@EnableScheduling
@Slf4j
public class BlogApplication implements CommandLineRunner {
    @Autowired
    RedisTemplate redisTemplate;

    public static void main(String[] args) {
        SpringApplication.run(BlogApplication.class, args);
        System.out.println("http://127.0.0.1:81");
    }

    /*
    避免向cookie写入加密后的字符串出现An invalid character [19] was present in the Cookie value
     */
    @Bean
    public WebServerFactoryCustomizer<TomcatServletWebServerFactory> cookieProcessorCustomizer() {
        return (factory) -> factory.addContextCustomizers(
                (context) -> context.setCookieProcessor(new LegacyCookieProcessor()));

    }

    //在Redis分布式锁那里，需要项目启动以后创建一个线程去查看消息队列有没有内容，如果有的话，就进行唤醒
    @Override
    public void run(String... args) throws Exception {
        //采用自旋锁方式，注释掉
        notifyThreadByList();
    }

    private void notifyThreadByList() {
        new Thread(() -> {
//            System.out.println(Thread.currentThread().getState());
//            log.info("id" + Thread.currentThread().getId());
            redisTemplate.opsForValue().set("notifyThreadId", Thread.currentThread().getId());
            String redisListKey = "viewList";
            String serverIp = null;
            String redisLock = "songYangLock";
            try {
                serverIp = IpUtils.getInnetIp();
             //   System.out.println("serverIp = " + serverIp);
                while (true) {
                    //查看有没有人拿到锁
                    String redisValue = (String) redisTemplate.opsForValue().get(redisLock);
                    //等于null说明这个时候没有人拿到锁，就尝试从消息队列里面出栈
                    if (null == redisValue) {
                        //redisTemplate.opsForList().
                        List<String> range = redisTemplate.opsForList().range(redisListKey, -1, -1);
                        //  log.info(range.size()+"");
                        if (null == range || range.size() == 0) continue;
                        String getRedisListName = range.get(0);
                        String[] split = getRedisListName.split("-");
                        if (split[0].equals(serverIp)) {//如果下一个出栈的ip是自己的话，就出栈这个信息，通过后面的id去找到这个线程
                            redisTemplate.opsForList().rightPop(redisListKey);//出栈
                            Thread AwakenedThread = findThread(Long.parseLong(split[1]));//通过线程的id去找到这个线程
                            log.info("唤醒线程：当前队列还有" + redisTemplate.opsForList().range(redisListKey, 0, -1).size() + "个元素等待");
                            synchronized (AwakenedThread) {
                                AwakenedThread.notify();//唤醒这个线程
                            }
                            //唤醒执行增长的线程，阻塞自己，等到执行线程来唤醒自己
//                            synchronized (Thread.currentThread()) {
//                                try {
//                                    Thread.currentThread().wait();
//                                } catch (InterruptedException e) {
//                                    e.printStackTrace();
//                                }
//                            }
                        }
                    } else {
                        log.info("唤醒线程：Redis锁正在被使用...休眠一段时间再进行");
//                        sleepWithMILLISECONDS(10);
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }).start();
    }

    private void sleepWithMILLISECONDS(int milliseconds) {
        try {
            TimeUnit.MILLISECONDS.sleep(milliseconds);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static Thread findThread(long threadId) {
        ThreadGroup group = Thread.currentThread().getThreadGroup();
        while (group != null) {
            Thread[] threads = new Thread[(int) (group.activeCount() * 1.2)];
            int count = group.enumerate(threads, true);
            for (int i = 0; i < count; i++) {
                if (threadId == threads[i].getId()) {
                    return threads[i];
                }
            }
            group = group.getParent();
        }
        return null;
    }
    /*
    热榜思路：
        因为排行榜是最近七天的评论数量最多的排行榜，所以项目启动的时候就在数据库查一下，
    当前时间和文章被发表的时间差在七天以内（这里用的sql的大于）文章缓存到Redis的hash
    结构。并且为每一个文章设置据他发表到七天以后的时间。
        当文章有评论更改的时候，首先在zset里面Redis自增，有删除评论就进行自减。因为每天
    凌晨同步Redis数据到数据库，然后删除Redis，所以Redis文章的评论数量的key是包含了是哪
    一天的哪一个文章，如果有新的评论的时候，就尝试从Redis里面去get，如果没有这个key，
    说明数据库刚完成Redis的同步更新，就去数据库查一下然后自加。 每一次删除增加评论的时候，
    都要对最近七天的评论Redis的zset做一次并集，这样就获得了最近七天的 热评数量。
        此外，对于恶意刷榜，给予认证，例如每一个用户在增加一条评论以后向Redis写入信息，
    然后每次写Redis的value就自增，如果大于一个阈值就给予前端一个认证的措施。

     */
    /*
    分布式锁思路
        当进行对访问量增长的时候，虽然可以使用自增的方式，但是为了复现分布式的情况，
    采用了get之后再set的情况，这样再分布式系统和多线程情况下就会产生线程安全的问题，
    可能在get之后自加再set的时候，有其他线程再get然后自加set，这样最终的结果就会
    产生很大的问题。
        所以采用了Redis锁方式，在进行增长访问量的时候，会开启一个线程，尝试去拿到锁，
    如果拿到了，就在锁的value写uuid，然后并且这是一个过期时间，这个是原子操作，然后
    进行逻辑操作，如果逻辑操作成功，判断这把锁的value是不是自己的uuid，如果是的话就
    删除这个锁。
        如果没有拿到锁，就会采用阻塞-唤醒方式 或者 自旋锁的方式，下文介绍。
     */
    /*
    阻塞-唤醒方式
         如果没有成功拿到锁，就先在Redis的list里面写入自己服务器的ip和自己线程id，
     因为在分布式系统中，多台服务同时工作，可能出现id重复，所以前面加上服务器ip来加以
     区分，写到list以后，线程阻塞自己。等待其他线程唤醒
         项目启动以后会开启一个线程，他的作用是监视Redis队列有没有任务，如果有查看当前
     锁有没有人正在用，如果没有人，对比最后要出队的是不是自己的线程，如果是，就出队，
     解析出他的线程id，然后通过线程组的方式遍历所有的线程，找到这个线程唤醒，取执行下面的逻辑

     优化：唤醒线程在队列中唤醒一个线程的时候，同时也会阻塞自己，等待增长线程执行完毕唤醒‘唤醒线程’之后，在继续从队列取数据执行
     这样减少了大量的无效轮询，但是线程的切换会浪费一定时间，待优化！
     */
    /*
    自旋锁的方式
        如果没有拿到锁，线程会入队，这个和上面的思路一样，然后这个线程会每隔一段时间看看
     即将要出队的线程是不是自己，如果是自己就出队，不断尝试拿到锁，如果拿到了就执行逻辑，
     如果不是自己则睡眠一段时间再去看。
     */

    /*
    Redis全局唯一ID实现思路
        在Redis中利用keyValue结构，key是当前时间的毫秒值，value默认是1，然后有一个
     线程访问就自增一次，最后拼接字符串，可以保证每一毫秒接收可以容long的最大值个线程。
        在实现的时候，首先去获取当前的时间戳，然后利用Redis的setnx命令，设置当前的毫秒值
     的value是1，如果当前已经有线程设置好了，那么这条命令就不会成功，自增就可以了，最后
     拼接自增结果和时间戳。
     */
    /*
    cookie加密实现思路
        当用户登录之后，会对登录的信息进行AES的加密，这种加密方式是对称加密，即加密和解密
     都是采用相同的公钥，所以这个公钥的保密尤为重要，不能够写死。
        在进行获取秘钥的时候，首先可以利用Redis全局id作为Redis的key，秘钥作为value，
     最后把这个唯一的id写入cookie，在用的时候先通过cookie找到唯一id找到公钥，然后进行
     解密，再重新登录。

        考虑到这个会造成Redis空间的大量浪费，可以更改秘钥的key，比如每7天换一次秘钥。
     登录的时候获取时间，在Redis当中查看这个时间点的的秘钥是什么，在进行解密。
     */
    /*
    多端登录其他端被踢下线思路
        在登录的时候，会向session和Redis写入信息，key是用户名，value是Redis全局唯一ID，
    在每一次请求的时候，会对比session和Redis的信息是否一致，如果发生了不一致，说明已经有人
    修改了当前用户Redis上的新的全局id，但是因为浏览器还在打开，他不会修改session当中当前用户的信息。
    造成了这个用户的信息不一致，说明当前用户被踢下线，执行注销操作。可以在这个时候去给予前端
    提示。
        Redis上的信息代表了这个用户最新的登录信息，session上的信息代表了这个用户在这太设备
    上的登录信息，如果一致，说明没有人抢占，如果不一致，说明有人修改Redis信息，不一致的一端
    被踢下线。
     */
}